interface TouchEnterLeaveEvent {
  element: Element;
  onEnter: (() => void)[];
  onLeave: (() => void)[];
  inside: boolean;
}

export class TouchEnterLeave {
  static readonly events: TouchEnterLeaveEvent[] = [];

  static onTouchEnter(element: Element | null, callback: () => void) {
    if (!element) {
      return;
    }

    let event: TouchEnterLeaveEvent = TouchEnterLeave.getOrCreateEvent(element);

    event.onEnter.push(callback);
  }

  static onTouchLeave(element: Element | null, callback: () => void) {
    if (!element) {
      return;
    }

    let event: TouchEnterLeaveEvent = TouchEnterLeave.getOrCreateEvent(element);

    event.onLeave.push(callback);
  }

  private static getEvent(element: Element) {
    for (const event of TouchEnterLeave.events) {
      if (event.element !== element) {
        continue;
      }

      return event;
    }

    return null;
  }

  private static getOrCreateEvent(element: Element) {
    let event: TouchEnterLeaveEvent | null = TouchEnterLeave.getEvent(element);

    if (!event) {
      event = {
        element,
        onEnter: [],
        onLeave: [],
        inside: false,
      };
    }

    TouchEnterLeave.events.push(event);

    return event;
  }

  static removeAll(element: Element | null) {
    if (!element) {
      return;
    }

    let i = TouchEnterLeave.events.length;
    while (i-- > 0) {
      const event = TouchEnterLeave.events[i];
      if (event.element !== element) {
        continue;
      }

      TouchEnterLeave.events.splice(i, 1);
      break;
    }
  }

  static removeOnTouchEnter(
    element: Element | null,
    callback: (() => void) | null = null,
  ) {
    if (!element) {
      return;
    }

    const event = TouchEnterLeave.getEvent(element);
    if (!event) {
      return;
    }

    if (!callback) {
      event.onEnter = [];
    } else {
      const pos = event.onEnter.indexOf(callback);
      if (pos < 0) {
        return;
      }

      event.onEnter.splice(pos, 1);
    }
  }

  static removeOnTouchLeave(
    element: Element | null,
    callback: (() => void) | null = null,
  ) {
    if (!element) {
      return;
    }

    const event = TouchEnterLeave.getEvent(element);
    if (!event) {
      return;
    }

    if (!callback) {
      event.onLeave = [];
    } else {
      const pos = event.onLeave.indexOf(callback);
      if (pos < 0) {
        return;
      }

      event.onLeave.splice(pos, 1);
    }
  }

  static onTouchMove(ev: TouchEvent) {
    if (!ev.touches || ev.touches.length <= 0) {
      return;
    }

    const element = document.elementFromPoint(
      ev.touches[0].clientX,
      ev.touches[0].clientY,
    );

    for (const event of TouchEnterLeave.events) {
      if (event.inside) {
        if (
          element == null ||
          (event.element !== element && !event.element?.contains(element))
        ) {
          event.inside = false;
          event.onLeave.forEach((callback) => callback());
        }
      } else {
        if (element === event.element || event.element?.contains(element)) {
          event.inside = true;
          event.onEnter.forEach((callback) => callback());
        }
      }
    }
  }

  static register() {
    document.addEventListener('touchmove', TouchEnterLeave.onTouchMove);
  }

  static unregister() {
    document.removeEventListener('touchmove', TouchEnterLeave.onTouchMove);
  }
}

TouchEnterLeave.register();
